package semant.secondpass;

import semant.Env;
import semant.TypeChecker;
import symbol.ClassInfo;
import symbol.MethodInfo;
import symbol.Symbol;
import symbol.VarInfo;
import syntaxtree.And;
import syntaxtree.ArrayLength;
import syntaxtree.ArrayLookup;
import syntaxtree.BooleanType;
import syntaxtree.Call;
import syntaxtree.Equal;
import syntaxtree.Exp;
import syntaxtree.False;
import syntaxtree.Identifier;
import syntaxtree.IdentifierExp;
import syntaxtree.IdentifierType;
import syntaxtree.IntArrayType;
import syntaxtree.IntegerLiteral;
import syntaxtree.IntegerType;
import syntaxtree.LessThan;
import syntaxtree.Minus;
import syntaxtree.NewArray;
import syntaxtree.NewObject;
import syntaxtree.Not;
import syntaxtree.Plus;
import syntaxtree.This;
import syntaxtree.Times;
import syntaxtree.True;
import syntaxtree.TypeVisitorAdapter;
import syntaxtree.Type;
import util.List;

public class ExpHandler extends TypeVisitorAdapter
{
	private Env e;
	private ClassInfo ci;
	private MethodInfo mi;
	
	private ExpHandler(Env e, ClassInfo ci, MethodInfo mi)
	{
		this.e = e;
		this.ci = ci;
		this.mi = mi;
	}
	
	public static Type secondPass(Env e, ClassInfo ci, MethodInfo mi, Exp node)
	{
		ExpHandler h = new ExpHandler(e, ci, mi);
		return node.accept(h);
	}

	@Override
	public Type visit(And node)
	{
		Type ll = ExpHandler.secondPass(e, ci, mi, node.lhs);
		Type rr = ExpHandler.secondPass(e, ci, mi, node.rhs);
		
		if(!(ll instanceof BooleanType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado esquerdo do \"&&\"",
					"Esperado: boolean",
					"Encontrado: " + ll});
		}
		
		if(!(rr instanceof BooleanType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado direito do \"&&\"",
					"Esperado: boolean",
					"Encontrado: " + rr});
		}
		
		return node.type = new BooleanType(node.line, node.row);
	}
	
	@Override
	public Type visit(ArrayLength node)
	{
		Type arrayType = ExpHandler.secondPass(e, ci, mi, node.array);
		
		if (!(arrayType instanceof IntArrayType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para propriedade 'length'.",
					"Esperado: vetor de inteiro",
					"Encontrado: " + arrayType});
		}
		
		return node.type = new IntegerType(node.line, node.row);
	}
	
	@Override
	public Type visit(ArrayLookup node)
	{		
		Type arrayType = ExpHandler.secondPass(e, ci, mi, node.array);
		Type index = ExpHandler.secondPass(e, ci, mi, node.index);
		
		if (!(arrayType instanceof IntArrayType))
		{
			e.err.Error(node, new Object[]{"Objeto " + node.array + " nao eh um vetor",
					"Esperado: vetor de inteiro",
					"Encontrado: " + arrayType});
		}
		
		if (!(index instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para index do ArrayLookup",
					"Esperado: inteiro",
					"Encontrado: " + index});
		}
		
		return node.type = new IntegerType(node.line, node.row);
	}
	
	@Override
	public Type visit(Call node)
	{
		Type t = ExpHandler.secondPass(e, ci, mi, node.object);
		
		if (!(t instanceof IdentifierType))
		{
			e.err.Error(node, new Object[]{"Chamada de metodo aplicado a tipo invalido",
										"Esperado: <class>" + node.object,
										"Encontrado: " + t});
			
			return node.type = new IntegerType(node.line, node.row);
		}
		
		Symbol o = Symbol.symbol(((IdentifierType)t).name);
		Symbol m = Symbol.symbol(node.method.s);
		
		ClassInfo ci = e.classes.get(o);
		
		if(ci == null)
		{
			e.err.Error(node, new Object[]{"Classe " + o + " nao definida."});
			return node.type = new IntegerType(node.line, node.row);
		}
		
		MethodInfo mi = ci.methods.get(m);
		
		if(mi == null)
		{
			e.err.Error(node, new Object[]{"Metodo " + m + " nao definida para classe \'" + ((IdentifierType)t).name + "\'"});
			return node.type = new IntegerType(node.line, node.row);
		}
			
		int formalSize = mi.formals == null ? 0 : mi.formals.size();
		int actualSize = node.actuals == null ? 0 : node.actuals.size();
		
		// Verifica o numero de parametros
		if(formalSize != actualSize)
		{
			e.err.Error(node, new Object[]{"Numero de parametros invalidos para o metodo "+ ((IdentifierType)t).name  + "." + mi.name});
			return node.type = mi.type;
		}
		
		if (actualSize > 0)
		{
			// Verifica os tipos dos parametros
			List<VarInfo> formals;
			List<Exp> no;
			int i;
			
			for (formals = mi.formals, no = node.actuals, i = 1; formals != null; formals = formals.tail, no = no.tail, i++)
			{
				Type actuals = ExpHandler.secondPass(e, this.ci, this.mi, no.head);
				
				if(!TypeChecker.compareType(formals.head.type, actuals, e))
					e.err.Error(node, new Object[]{"Tipo do argumento #" + i + " para o metodo " + mi.decorateName() + " nao eh compativel.",
										"Esperado: " + formals.head.type,
										"Encontrado: " + actuals});
			}
		}
		
		return node.type = mi.type;
	}
	
	@Override
	public Type visit(Equal node)
	{
		Type ll = ExpHandler.secondPass(e, ci, mi, node.lhs);
		Type rr = ExpHandler.secondPass(e, ci, mi, node.rhs);
		
		if(!TypeChecker.compareType(ll, rr, e))
			e.err.Error(node, new Object[]{"Tipos diferentes na comparacao:",
										"Tipo do lado esquerdo: " + ll,
										"Tipo do lado direito: " + rr});
		
		
		return node.type = new BooleanType(node.line, node.row);
	}
	
	@Override
	public Type visit(False node)
	{
		return node.type = new BooleanType(node.line, node.row);
	}
	
	@Override
	public Type visit(IdentifierExp node)
	{
		VarInfo v = TypeChecker.getVariable(ci, mi, node.name.s);
		
		if(v == null)
		{
			TypeChecker.erroIdentificadorNaoDeclarado(e, node.name);
			return new IntegerType(node.line, node.row);
		}
		
		return node.type = v.type;
	}
	
	@Override
	public Type visit(IntegerLiteral node)
	{
		return node.type = new IntegerType(node.line, node.row);
	}
	
	@Override
	public Type visit(LessThan node)
	{
		Type ll = ExpHandler.secondPass(e, ci, mi, node.lhs);
		Type rr = ExpHandler.secondPass(e, ci, mi, node.rhs);
		
		if(!(ll instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado esquerdo do \'<\'",
					"Esperado: int",
					"Encontrado: " + ll});
		}
		
		if(!(rr instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado direito do \'<\'",
					"Esperado: int",
					"Encontrado: " + rr});
		}
		
		return node.type = new BooleanType(node.line, node.row);
	}
	
	@Override
	public Type visit(Minus node)
	{
		Type ll = ExpHandler.secondPass(e, ci, mi, node.lhs);
		Type rr = ExpHandler.secondPass(e, ci, mi, node.rhs);
		
		if(!(ll instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado esquerdo do \'-\'",
					"Esperado: int",
					"Encontrado: " + ll});
		}
		
		if(!(rr instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado direito do \'-\'",
					"Esperado: int",
					"Encontrado: " + rr});
		}
		
		return node.type = new IntegerType(node.line, node.row);
	}
	
	@Override
	public Type visit(NewArray node)
	{
		Type size = ExpHandler.secondPass(e, ci, mi, node.size);
		
		if(!(size instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para o tamanho do array do \'int[size]\'",
					"Esperado: int",
					"Encontrado: " + size});
		}
		
		return node.type = new IntArrayType(node.line, node.row);
	}
	
	@Override
	public Type visit(NewObject node)
	{
		Symbol name = Symbol.symbol(node.className.s);
		ClassInfo ci = e.classes.get(name);
		
		if(ci == null)
		{
			e.err.Error(node, new Object[]{"Objeto nao pode ser criado, classe \"" + name + "\" nao definida."});
			return node.type = new IntegerType(node.line, node.row);
		}
		
		return node.type = new IdentifierType(node.line, node.row, ci.name.toString());
	}
	
	@Override
	public Type visit(Not node)
	{
		Type not = ExpHandler.secondPass(e, ci, mi, node.exp);
		
		if(!(not instanceof BooleanType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para \'!\'.",
					"Esperado: boolean",
					"Encontrado: " + not});
		}
		
		return node.type = new BooleanType(node.line, node.row);
	}

	@Override
	public Type visit(Plus node)
	{
		Type ll = ExpHandler.secondPass(e, ci, mi, node.lhs);
		Type rr = ExpHandler.secondPass(e, ci, mi, node.rhs);
		
		if(!(ll instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado esquerdo do \'+\'",
					"Esperado: int",
					"Encontrado: " + ll});
		}
		
		if(!(rr instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado direito do \'+\'",
					"Esperado: int",
					"Encontrado: " + rr});
		}
		
		return node.type = new IntegerType(node.line, node.row);
	}
	
	@Override
	public Type visit(This node)
	{
		// como "main" eh palavra reservada, apenas o metodo main terá esse nome
		if (mi.name == Symbol.symbol("main"))
		{
			e.err.Error(node, new Object[]{"Uso não permitido de \"this\" em contexto estático."});
		}
		
		return node.type = new IdentifierType(node.line, node.row, ci.name.toString());
	}
	
	@Override
	public Type visit(Times node)
	{
		Type ll = ExpHandler.secondPass(e, ci, mi, node.lhs);
		Type rr = ExpHandler.secondPass(e, ci, mi, node.rhs);
		
		if(!(ll instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado esquerdo do \'*\'",
					"Esperado: int",
					"Encontrado: " + ll});
		}
		
		if(!(rr instanceof IntegerType))
		{
			e.err.Error(node, new Object[]{"Tipo inválido para lado direito do \'*\'",
					"Esperado: int",
					"Encontrado: " + rr});
		}
		
		return node.type = new IntegerType(node.line, node.row);
	}
	
	@Override
	public Type visit(True node)
	{
		return node.type = new BooleanType(node.line, node.row);
	}
}
